package bancosys.tec.persist.hibernate.eventListener;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.hibernate.action.CollectionRecreateAction;
import org.hibernate.action.CollectionRemoveAction;
import org.hibernate.action.CollectionUpdateAction;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.engine.ActionQueue;
import org.hibernate.engine.Cascade;
import org.hibernate.engine.CollectionEntry;
import org.hibernate.engine.CollectionKey;
import org.hibernate.engine.Collections;
import org.hibernate.engine.EntityEntry;
import org.hibernate.engine.PersistenceContext;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.Status;
import org.hibernate.event.EventSource;
import org.hibernate.event.FlushEntityEvent;
import org.hibernate.event.FlushEntityEventListener;
import org.hibernate.event.FlushEvent;
import org.hibernate.event.def.AbstractFlushingEventListener;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.Printer;
import org.hibernate.util.IdentityMap;
import org.hibernate.util.LazyIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import bancosys.tec.persist.DirtyAware;

/**
 * @author takeshi
 */
public class AbstractDirtyAwareFlushEventListener extends AbstractFlushingEventListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(DirtyAwareFlushEventListener.class);

    /**
     * Coordinates the processing necessary to get things ready for executions as db calls by preping the session caches and moving the
     * appropriate entities and collections to their respective execution queues.
     * 
     * @param event The flush event.
     * @Error flushing caches to execution queues.
     */
    @Override
    @SuppressWarnings("unchecked")
    protected void flushEverythingToExecutions(FlushEvent event) {

        LOGGER.trace("flushing session");

        EventSource session = event.getSession();

        final PersistenceContext persistenceContext = session.getPersistenceContext();
        session.getInterceptor().preFlush(new LazyIterator(persistenceContext.getEntitiesByKey()));

        this.prepareEntityFlushes(session, filterNonDirty(IdentityMap.concurrentEntries(persistenceContext.getEntityEntries())));

        final Entry<Object, EntityEntry>[] list = filterNonDirty(IdentityMap.concurrentEntries(persistenceContext.getEntityEntries()));
        // we could move this inside if we wanted to
        // tolerate collection initializations during
        // collection dirty checking:
        this.prepareCollectionFlushes(session, IdentityMap.entries(persistenceContext.getCollectionEntries()));
        // now, any collections that are initialized
        // inside this block do not get updated - they
        // are ignored until the next flush

        persistenceContext.setFlushing(true);
        try {
            this.flushEntities(event, list);
            this.flushCollections(session, filterCollectionEntries(persistenceContext, IdentityMap.entries(persistenceContext.getCollectionEntries())));
        } finally {
            persistenceContext.setFlushing(false);
        }

        // some statistics
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Flushed: " + session.getActionQueue().numberOfInsertions() + " insertions, "
                    + session.getActionQueue().numberOfUpdates() + " updates, " + session.getActionQueue().numberOfDeletions()
                    + " deletions to " + persistenceContext.getEntityEntries().size() + " objects");
            LOGGER.debug("Flushed: " + session.getActionQueue().numberOfCollectionCreations() + " (re)creations, "
                    + session.getActionQueue().numberOfCollectionUpdates() + " updates, "
                    + session.getActionQueue().numberOfCollectionRemovals() + " removals to "
                    + persistenceContext.getCollectionEntries().size() + " collections");
            new Printer(session.getFactory()).toString(persistenceContext.getEntitiesByKey().values().iterator(), session.getEntityMode());
        }
    }

    /**
     * @param ctx {@link PersistenceContext}
     * @param entries List of collection entries
     * @param dirtyState
     * @return List
     */
    private List<Entry<PersistentCollection, CollectionEntry>> filterCollectionEntries(PersistenceContext ctx,
            List<Entry<PersistentCollection, CollectionEntry>> entries) {
        List<Entry<PersistentCollection, CollectionEntry>> r = new LinkedList<Entry<PersistentCollection, CollectionEntry>>();
        for (Entry<PersistentCollection, CollectionEntry> entry : entries) {
            Object owner = entry.getKey().getOwner();
            if (!shouldSkipDirtyAware(owner, ctx.getEntry(owner))) {
                r.add(entry);
            } else {
                entry.getValue().setProcessed(true);
            }
        }
        return r;
    }

    /**
     * @param concurrentEntries original array
     * @param dirtyState
     * @return array without non dirty entities
     */
    @SuppressWarnings("unchecked")
    private Entry<Object, EntityEntry>[] filterNonDirty(Entry<Object, EntityEntry>[] concurrentEntries) {
        final int n = concurrentEntries.length;
        Entry<Object, EntityEntry>[] result = new Entry[n];
        int j = 0, i = 0;
        for (; i < n; i++) {
            final Entry<Object, EntityEntry> each = concurrentEntries[i];
            Object owner = each.getKey();
            EntityEntry ee = each.getValue();
            if (!shouldSkipDirtyAware(owner, ee)) {
                result[j++] = each;
            }
        }
        if (n == j) {
            return result;
        }
        // must trim the resulting array
        Entry<Object, EntityEntry>[] copy = new Entry[j];
        System.arraycopy(result, 0, copy, 0, j);
        return copy;
    }

    /**
     * @param session {@link SessionImplementor} 1. Recreate the collection key -> collection map 2. rebuild the collection entries 3. call
     * Interceptor.postFlush()
     */
    @Override
    @SuppressWarnings("unchecked")
    protected void postFlush(SessionImplementor session) {
        LOGGER.trace("post flush");

        final PersistenceContext persistenceContext = session.getPersistenceContext();
        persistenceContext.getCollectionsByKey().clear();
        persistenceContext.getBatchFetchQueue().clearSubselects(); // the database has changed now, so the subselect results need to be
        // invalidated
        Iterator iter = persistenceContext.getCollectionEntries().entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry me = (Map.Entry) iter.next();
            CollectionEntry collectionEntry = (CollectionEntry) me.getValue();
            PersistentCollection persistentCollection = (PersistentCollection) me.getKey();

            // tratamento de dirty aware.
            Object owner = persistentCollection.getOwner();
            if (this.shouldSkipDirtyAware(owner, persistenceContext.getEntry(owner))) {
                collectionEntry.setProcessed(true);
            }
            // fim do tratamento de dirty aware.

            collectionEntry.postFlush(persistentCollection);
            if (collectionEntry.getLoadedPersister() == null) {
                // if the collection is dereferenced, remove from the session cache
                // iter.remove(); //does not work, since the entrySet is not backed by the set
                persistenceContext.getCollectionEntries().remove(persistentCollection);
            } else {
                // otherwise recreate the mapping between the collection and its key
                CollectionKey collectionKey =
                        new CollectionKey(collectionEntry.getLoadedPersister(), collectionEntry.getLoadedKey(), session.getEntityMode());
                persistenceContext.getCollectionsByKey().put(collectionKey, persistentCollection);
            }
        }

        session.getInterceptor().postFlush(new LazyIterator(persistenceContext.getEntitiesByKey()));

        LOGGER.debug("cleaning dirty aware entities");
        final Map.Entry[] list = IdentityMap.concurrentEntries(session.getPersistenceContext().getEntityEntries());
        // safe from concurrent modification because of how entryList() is implemented on IdentityMap
        final int size = list.length;
        for (int i = 0; i < size; i++) {
            Map.Entry me = list[i];

            Object entity = me.getKey();
            if (entity instanceof DirtyAware) {
                DirtyAware dirtyAwareEntity = (DirtyAware) entity;
                dirtyAwareEntity.setDirty(false);
            }
        }
    }

    /**
     * process cascade save/update at the start of a flush to discover any newly referenced entity that must be passed to saveOrUpdate(),
     * and also apply orphan delete
     * 
     * @param session session
     * @param list array de {@link Entry}
     */
    private void prepareEntityFlushes(EventSource session, Entry<Object, EntityEntry>[] list) {
        LOGGER.debug("processing flush-time cascades");

        // safe from concurrent modification because of how entryList() is implemented on IdentityMap
        final int size = list.length;
        final Object anything = this.getAnything();
        for (int i = 0; i < size; i++) {
            Entry<Object, EntityEntry> me = list[i];
            EntityEntry entry = me.getValue();
            Status status = entry.getStatus();
            if (status == Status.MANAGED || status == Status.SAVING) {
                this.cascadeOnFlush(session, entry.getPersister(), me.getKey(), anything);
            }
        }
    }

    /**
     * @param session s
     * @param persister p
     * @param object a
     * @param anything a
     */
    private void cascadeOnFlush(EventSource session, EntityPersister persister, Object object, Object anything) {
        session.getPersistenceContext().incrementCascadeLevel();
        try {
            new Cascade(this.getCascadingAction(), Cascade.BEFORE_FLUSH, session).cascade(persister, object, anything);
        } finally {
            session.getPersistenceContext().decrementCascadeLevel();
        }
    }

    /**
     * Initialize the flags of the CollectionEntry, including the dirty check.
     * 
     * @param session session
     * @param list the list
     * @param dirtyState
     */
    private void prepareCollectionFlushes(SessionImplementor session, List<Entry<PersistentCollection, CollectionEntry>> list) {
        // Initialize dirty flags for arrays + collections with composite elements
        // and reset reached, doupdate, etc.
        LOGGER.debug("dirty checking collections");
        for (Entry<PersistentCollection, CollectionEntry> entry : list) {
            (entry.getValue()).preFlush(entry.getKey());
        }
    }

    /**
     * @param owner object
     * @param entity the EntityEntry containing the object
     * @return boolean
     */
    private boolean shouldSkipDirtyAware(Object owner, EntityEntry entity) {
        if (owner instanceof DirtyAware && !((DirtyAware) owner).isDirty()) {
            return entity != null && (entity.getStatus() == Status.MANAGED || entity.getStatus() == Status.READ_ONLY);
        }

        return false;
    }

    /**
     * 1. detect any dirty entities 2. schedule any entity updates 3. search out any reachable collections
     * 
     * @param event event
     * @param list array de {@link Entry}
     */
    private void flushEntities(FlushEvent event, Entry<Object, EntityEntry>[] list) {

        LOGGER.trace("Flushing entities and processing referenced collections");

        // Among other things, updateReachables() will recursively load all
        // collections that are moving roles. This might cause entities to
        // be loaded.

        // So this needs to be safe from concurrent modification problems.
        // It is safe because of how IdentityMap implements entrySet()

        final EventSource source = event.getSession();

        final int size = list.length;
        for (int i = 0; i < size; i++) {

            // Update the status of the object and if necessary, schedule an update

            Entry<Object, EntityEntry> me = list[i];
            EntityEntry entry = me.getValue();
            Status status = entry.getStatus();

            if (status != Status.LOADING && status != Status.GONE) {
                FlushEntityEvent entityEvent = new FlushEntityEvent(source, me.getKey(), entry);
                FlushEntityEventListener[] listeners = source.getListeners().getFlushEntityEventListeners();
                for (int j = 0; j < listeners.length; j++) {
                    listeners[j].onFlushEntity(entityEvent);
                }
            }
        }

        source.getActionQueue().sortActions();
    }

    /**
     * process any unreferenced collections and then inspect all known collections, scheduling creates/removes/updates
     * 
     * @param session session
     * @param collections the list of entries
     */
    @SuppressWarnings("unchecked")
    private void flushCollections(EventSource session, List<Entry<PersistentCollection, CollectionEntry>> collections) {
        LOGGER.trace("Processing unreferenced collections");
        final PersistenceContext pc = session.getPersistenceContext();
        for (Entry<PersistentCollection, CollectionEntry> entry : collections) {
            CollectionEntry collectionEntry = entry.getValue();
            if (!collectionEntry.isReached() && !collectionEntry.isIgnore()) {
                Collections.processUnreachableCollection(entry.getKey(), session);
            }
        }
        // Schedule updates to collections:

        LOGGER.trace("Scheduling collection removes/(re)creates/updates");

        List<Entry<PersistentCollection, CollectionEntry>> list =
                IdentityMap.entries(session.getPersistenceContext().getCollectionEntries());
        ActionQueue actionQueue = session.getActionQueue();
        for (Entry<PersistentCollection, CollectionEntry> me : list) {
            PersistentCollection coll = me.getKey();
            CollectionEntry ce = me.getValue();
            Object owner = me.getKey().getOwner();

            if (this.shouldSkipDirtyAware(owner, pc.getEntry(owner))) {
                continue;
            }

            if (ce.isDorecreate()) {
                session.getInterceptor().onCollectionRecreate(coll, ce.getCurrentKey());
                actionQueue.addAction(new CollectionRecreateAction(coll, ce.getCurrentPersister(), ce.getCurrentKey(), session));
            }
            if (ce.isDoremove()) {
                session.getInterceptor().onCollectionRemove(coll, ce.getLoadedKey());
                actionQueue.addAction(new CollectionRemoveAction(coll, ce.getLoadedPersister(), ce.getLoadedKey(),
                        ce.isSnapshotEmpty(coll), session));
            }
            if (ce.isDoupdate()) {
                session.getInterceptor().onCollectionUpdate(coll, ce.getLoadedKey());
                actionQueue.addAction(new CollectionUpdateAction(coll, ce.getLoadedPersister(), ce.getLoadedKey(),
                        ce.isSnapshotEmpty(coll), session));
            }

        }

        actionQueue.sortCollectionActions();

    }
}
